/**
 * Copyright [2019] [LiBo/Alex of copyright liboware@gmail.com ]
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hyts.hermes.ssoLogin.saml.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.hyts.hermes.ssoLogin.saml.config.OktaSamlParameter;
import com.onelogin.saml2.Auth;
import com.onelogin.saml2.settings.Saml2Settings;
import com.onelogin.saml2.settings.SettingsBuilder;
import com.onelogin.saml2.util.Util;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.core.*;
import org.opensaml.saml2.core.impl.LogoutResponseImpl;
import org.opensaml.xml.Configuration;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.io.Unmarshaller;
import org.opensaml.xml.io.UnmarshallerFactory;
import org.opensaml.xml.schema.impl.XSStringImpl;
import org.opensaml.xml.security.x509.BasicX509Credential;
import org.opensaml.xml.signature.Signature;
import org.opensaml.xml.signature.SignatureValidator;
import org.opensaml.xml.util.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
import java.util.Map;

/**
 * @project-name:hermes
 * @package-name:com.hyts.hermes.ssoLogin.saml.impl
 * @author:LiBo/Alex
 * @create-date:2023-04-10 14:02
 * @copyright:libo-alex4java
 * @email:liboware@gmail.com
 * @description:
 */

@Slf4j
@Component
public class DefaultOktaClientSSOService {


    private static final BasicX509Credential credential = new BasicX509Credential();


    private static final String pattern = "yyyy-MM-dd HH:mm:ss";


    @Autowired
    private OktaSamlParameter oktaSamlParameter;

    /**
     * 解密操作Saml2回调的消息数据(idpx509)
     *
     * @param idpx509CertContent
     * @return
     */
    public SignatureValidator decryptSaml2CallBackIdpx509ToSignatureValidator(String idpx509CertContent) {
        // 字节数组数据流
        Preconditions.checkArgument(StringUtils.isNotBlank(idpx509CertContent), "idpx509CertContent");
        // 尝试处理字节流处理数据
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.decode(idpx509CertContent))) {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(byteArrayInputStream);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(certificate.getPublicKey().getEncoded());
            PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
            credential.setPublicKey(publicKey);
            return new SignatureValidator(credential);
        } catch (CertificateException | NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
            log.error("[hermes-doLogin] decryptSaml2CallBackIdpx509Message is failure ", e);
            return null;
        }
    }


    /**
     * 校验和验证响应信息
     *
     * @param responseMsg
     * @return
     */
    public UserModel validateAndAnalysisSaml2EntityForLogin(String responseMsg, SignatureValidator signatureValidator) {
        // 处理数据流控制机制
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.decode(responseMsg))) {
            DefaultBootstrap.bootstrap();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.parse(inputStream);
            Element documentElement = document.getDocumentElement();
            UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
            Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(documentElement);
            XMLObject xmlObject = unmarshaller.unmarshall(documentElement);
            Response response = (Response) xmlObject;
            Assertion assertion = CollectionUtil.getFirst(response.getAssertions());
            //验证签名
            Signature signature = assertion.getSignature();
            //签名验证失败会抛错
            signatureValidator.validate(signature);
            //校验是否登录成功
            String successStr = response.getStatus().getStatusCode().getValue();
            // 包含对应的成功状态标识
            Preconditions.checkArgument(StringUtils.contains(successStr, "Success"), "[hermes-doLogin] validateAndAnalysisSaml2EntityForLogin is not success - resultMessage:%s", response.getStatus().getStatusCode().getValue());
            //检验登录时间
            DateTime notBefore = assertion.getConditions().getNotBefore();
            DateTime notOnOrAfter = assertion.getConditions().getNotOnOrAfter();
            DateTime now = new DateTime(DateTimeZone.UTC);
            log.info("will check the okta platform return data - notBefore={} - notOnOrAfter={} - now={}", notBefore.toString(pattern), notOnOrAfter.toString(pattern), now.toString(pattern));
            Preconditions.checkArgument(!(now.isBefore(notBefore) || now.isAfter(notOnOrAfter)), "login success date check is failure, notBefore={}, notOnOrAfter: %s, now={}", notBefore.toString(pattern), notOnOrAfter.toString(pattern), now.toString(pattern));
            //返回登录用户名
            List<AttributeStatement> attributeStatementList = assertion.getAttributeStatements();
            // 判断attributeStatements集合不为空！
            Map<String,String> userProperties = Maps.newHashMap();
            if(CollectionUtil.isNotEmpty(attributeStatementList)){
                for(AttributeStatement attributeStatement: attributeStatementList){
                    List<Attribute> attributeList = attributeStatement.getAttributes();
                    for(Attribute attribute : attributeList){
                        if(CollectionUtil.isNotEmpty(attribute.getAttributeValues())){
                            XSStringImpl xsString = (XSStringImpl) CollectionUtil.getFirst(attribute.getAttributeValues());
                            userProperties.put(attribute.getName(),xsString.getValue());
                        }
                    }
                }
            }
            return new UserModel(assertion.getSubject().getNameID().getValue(),userProperties);
        } catch (Exception e) {
            log.error("[hermes-ssoLogin] - validate SAMLResponse failure", e);
        }
        return null;
    }

    /**
     * 解析校验分析对应的等处操作
     * @param responseMsg
     * @param signatureValidator
     * @return
     */
    public UserModel validateAndAnalysisSaml2EntityForLogout(String responseMsg, SignatureValidator signatureValidator) {
        // 处理数据流控制机制
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64.decode(responseMsg))) {
            DefaultBootstrap.bootstrap();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.parse(inputStream);
            Element documentElement = document.getDocumentElement();
            UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
            Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(documentElement);
            XMLObject xmlObject = unmarshaller.unmarshall(documentElement);
            LogoutResponseImpl response = (LogoutResponseImpl) xmlObject;
            String successStr = response.getStatus().getStatusCode().getValue();
            // 包含对应的成功状态标识
            Preconditions.checkArgument(StringUtils.contains(successStr, "Success"), "[hermes-doLogin] validateAndAnalysisSaml2EntityForLogin is not success - resultMessage:%s", response.getStatus().getStatusCode().getValue());
            return new UserModel();
        } catch (Exception e) {
            log.error("[hermes-ssoLogin] - validate SAMLResponse failure", e);
        }
        return null;
    }

    /**
     * 加载证书信息数据
     * @param propValue
     * @return
     */
    protected X509Certificate loadCertificateFromProp(Object propValue) {
        if (ObjectUtils.isNotEmpty(propValue)) {
            try {
                return Util.loadCert(((String)propValue).trim());
            } catch (CertificateException var3) {
                return null;
            }
        } else {
            return propValue instanceof X509Certificate ? (X509Certificate)propValue : null;
        }
    }


    /**
     * 获取认证服务对象机制
     * @param request
     * @param response
     * @return
     */
    public Auth createAuthEntity(HttpServletRequest request, HttpServletResponse response, OktaSamlParameter oktaSamlParameter){
        Saml2Settings saml2Setting = new SettingsBuilder().build();
        try {
            // 设置对应的实例id
            if (StringUtils.isNotEmpty(oktaSamlParameter.getSpEntityid())) {
                BeanUtil.setFieldValue(saml2Setting,"spEntityId", oktaSamlParameter.getSpEntityid());
            }
            // 设置对应的断言服务ACS路径接口
            if (StringUtils.isNotEmpty(oktaSamlParameter.getSpAssertionConsumerServiceUrl())) {
                BeanUtil.setFieldValue(saml2Setting,"spAssertionConsumerServiceUrl",new URL(oktaSamlParameter.getSpAssertionConsumerServiceUrl()));
            }
            // 设置登出接口操作
            if (StringUtils.isNotEmpty(oktaSamlParameter.getSpSingleLogoutServiceUrl())) {
                BeanUtil.setFieldValue(saml2Setting,"spSingleLogoutServiceUrl",new URL(oktaSamlParameter.getSpSingleLogoutServiceUrl()));
            }
            // 设置对应的x509的数据信息
            if (StringUtils.isNotEmpty(oktaSamlParameter.getSpX509cert())) {
                BeanUtil.setFieldValue(saml2Setting,"spX509cert",loadCertificateFromProp(oktaSamlParameter.getSpX509cert()));
            }

            /**************************************************************************************/

            // 设置对应的实例id
            if (StringUtils.isNotEmpty(oktaSamlParameter.getIdpEntityid())) {
                BeanUtil.setFieldValue(saml2Setting,"idpEntityId", oktaSamlParameter.getIdpEntityid());
            }
            // 设置对应的断言服务ACS路径接口
            if (StringUtils.isNotEmpty(oktaSamlParameter.getIdpSingleSignOnServiceUrl())) {
                BeanUtil.setFieldValue(saml2Setting,"idpSingleSignOnServiceUrl",new URL(oktaSamlParameter.getIdpSingleSignOnServiceUrl()));
            }
            // 设置登出接口操作
            if (StringUtils.isNotEmpty(oktaSamlParameter.getIdpSingleLogoutServiceUrl())) {
                BeanUtil.setFieldValue(saml2Setting,"idpSingleLogoutServiceUrl",new URL(oktaSamlParameter.getIdpSingleLogoutServiceUrl()));
            }
            // 设置对应的x509的数据信息
            if (StringUtils.isNotEmpty(oktaSamlParameter.getIdpX509cert())) {
                BeanUtil.setFieldValue(saml2Setting,"idpx509cert",loadCertificateFromProp(oktaSamlParameter.getIdpX509cert()));
            }
            return new Auth(saml2Setting,request, response);
        } catch (Exception e) {
            log.error("createAuthEntity is failure!",e);
            throw new RuntimeException("createAuthEntity is failure!",e);
        }
    }

    /**
     * 获取对应的
     * @return
     */
    public OktaSamlParameter getOktaSamlPropertiesModel(Long uuid){
        return oktaSamlParameter;
    }

    /**
     * @project-name:hermes
     * @package-name:com.hyts.hermes.ssoLogin.saml
     * @author:LiBo/Alex
     * @create-date:2023-04-10 14:04
     * @copyright:libo-alex4java
     * @email:liboware@gmail.com
     * @description:
     */
    @ToString
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class UserModel implements Serializable {

        /**
         * 用户名
         */
        private String userName;

        /**
         * 用户熟悉信息
         */
        private Map<String, String> userProperties;
    }
}
